package org.jing.ext.sftp;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import org.jing.core.lang.JingException;
import org.jing.core.logger.JingLogger;
import org.jing.core.util.GenericUtil;
import org.jing.core.util.StringUtil;
import org.jing.ext.example.Example4SftpOperator;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Properties;

/**
 * Description: SFTP操作对象. <br>
 *     建议使用申请资源方式创建对象. <br>
 *
 * @author bks <br>
 * @see Example4SftpOperator#Example4SftpOperator()  参考用例 <br>
 * @since 2020-12-07 <br>
 */
@SuppressWarnings({ "WeakerAccess", "Duplicates", "UnusedReturnValue", "unused", "unchecked" })
public class SftpOperator implements Closeable {
    private static final JingLogger LOGGER = JingLogger.getLogger(SftpOperator.class);

    private final ChannelSftp sftpChannel;

    private ChannelExec execChannel = null;

    private final Session sshSession;

    private int hashCode;

    private boolean closed = false;

    private SftpOperator(String ip, int port, String name, String password, long timeout) {
        hashCode = hashCode();
        LOGGER.debug("[{}] Connect to sftpChannel", hashCode);
        try {
            LOGGER.debug("[{}] Address: {}:{}", hashCode, ip, port);
            LOGGER.debug("[{}] User: {}", hashCode, name);
            JSch jsch = new JSch();
            sshSession = jsch.getSession(name, ip, port);
            sshSession.setPassword(password);
            Properties sshConfig = new Properties();
            sshConfig.put("StrictHostKeyChecking", "no");
            sshSession.setConfig(sshConfig);
            sshSession.connect();
            LOGGER.debug("[{}] Open sftp channel", hashCode);
            sftpChannel = (ChannelSftp) sshSession.openChannel("sftp");
            sftpChannel.connect(timeout > 0 ? (int) Math.min(timeout, Integer.MAX_VALUE) : 0);
            LOGGER.debug("[{}] Success to connect", hashCode);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    @Override protected void finalize() throws Throwable {
        super.finalize();
        try {
            if (!closed) {
                bye();
            }
        }
        catch (Throwable ignored) {}
    }

    /**
     * Description: 指定参数获取封装操作对象. <br>
     *
     * @param ip IP地址 <br>
     * @param port 端口 <br>
     * @param name 登录名 <br>
     * @param password 登录密码 <br>
     * @return 封装操作对象 <br>
     */
    public static SftpOperator connect(String ip, int port, String name, String password) {
        return new SftpOperator(ip, port, name, password, 0);
    }

    /**
     * Description: 指定参数获取封装操作对象. <br>
     *
     * @param ip IP地址 <br>
     * @param port 端口 <br>
     * @param name 登录名 <br>
     * @param password 登录密码 <br>
     * @param timeout 超时时间 <br>
     * @return 封装操作对象 <br>
     */
    public static SftpOperator connect(String ip, int port, String name, String password, long timeout) {
        return new SftpOperator(ip, port, name, password, timeout);
    }

    /**
     * Description: 断开连接. <br>
     *     如果不主动调用, 析构函数里也会调用, 但还是建议自己手动关闭连接. <br>
     */
    public void bye() {
        try {
            if (closed) {
                LOGGER.debug("[{}] SFtp has closed", hashCode);
            }
            else {
                synchronized (sshSession) {
                    if (sshSession.isConnected()) {
                        sshSession.disconnect();
                    }
                    if (!sftpChannel.isClosed()) {
                        sftpChannel.disconnect();
                    }
                    if (null != execChannel && !execChannel.isClosed()) {
                        execChannel.disconnect();
                    }
                    LOGGER.debug("[{}] SFtp closed", hashCode);
                    closed = true;
                }
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    private void validateActive() {
        synchronized (sshSession) {
            if (closed) {
                throw new JingException("SFtp client has already closed");
            }
        }
    }

    /**
     * Description: 切换目录. <br>
     * @param path 指定目录 <br>
     * @return true:成功/false:失败 <br>
     */
    public boolean cd(String path) {
        validateActive();
        boolean result;
        LOGGER.debug("[{}] Change directory to path: [path: {}]", hashCode, path);
        if (StringUtil.isEmpty(path)) {
            LOGGER.debug("[{}] Cancel change because of empty path", hashCode);
            return true;
        }
        try {
            try {
                sftpChannel.cd(path);
                result = true;
            }
            catch (Throwable e) {
                result = false;
            }
            LOGGER.debug("[{}] Try to change directory by full path: [path: {}][result: {}]", hashCode, path, result);
            if (result) {
                return true;
            }
            LOGGER.debug("[{}] Failed to change directory by full path, try to change by each sub directory", hashCode);
            int length = path.length();
            char c;
            StringBuilder stbr = new StringBuilder();
            stbr.append(c = path.charAt(0));
            String subPath;
            for (int i$ = 1; i$ <= length; i$++) {
                if (i$ != length) {
                    c = path.charAt(i$);
                    stbr.append(c);
                }
                if (c == '/' || i$ == length) {
                    if (stbr.length() == 0) {
                        break;
                    }
                    subPath = stbr.toString();
                    try {
                        sftpChannel.cd(subPath);
                        result = true;
                    }
                    catch (Throwable e) {
                        result = false;
                    }
                    LOGGER.debug("[{}] Change to sub directory: [subPath: {}][result: {}]", hashCode, subPath, result);
                    if (!result) {
                        try {
                            sftpChannel.mkdir(subPath);
                            result = true;
                        }
                        catch (Exception ignored) {}
                        LOGGER.debug("[{}] Try to make directory: [subPath: {}][result: {}]", hashCode, subPath, result);
                        if (!result) {
                            LOGGER.warn("[{}] Failed to make sub directory", hashCode);
                            return false;
                        }
                        try {
                            sftpChannel.cd(subPath);
                            result = true;
                        }
                        catch (Exception ignored) {
                            result = false;
                        }
                        LOGGER.debug("[{}] Change to sub directory: [subPath: {}][result: {}]", hashCode, subPath, result);
                        if (!result) {
                            LOGGER.warn("[{}] Failed to change directory to sub directory", hashCode);
                            return false;
                        }
                    }
                    stbr.setLength(0);
                }
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
        LOGGER.debug("[{}] Success to change directory: [path: {}]", hashCode, path);
        return true;
    }

    /**
     * Description: 列举出当前目录下所有文件. <br>
     * @return 文件列表 <br>
     */
    public ArrayList<SftpFile> ls() {
        validateActive();
        try {
            LOGGER.debug("[{}] SFtp list all files...", hashCode);
            ArrayList<SftpFile> fileList = new ArrayList<>();
            ListIterator<ChannelSftp.LsEntry> iterator = sftpChannel.ls(sftpChannel.pwd()).listIterator();
            String fileName;
            SftpFile file;
            while (iterator.hasNext()) {
                fileList.add(file = SftpFile.parse(iterator.next()));
                LOGGER.debug("[name: {}][type: {}][size: {} bytes]", file.getFileName(), !file.getFileAttributes().isDir() ? "file" : "directory", file.getFileAttributes().getSize());
            }
            return fileList;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 检查当前路径下是否存在指定文件名的文件. <br>
     * @param fileName 指定文件名 <br>
     * @return <br>
     */
    public SftpFile ls(String fileName) {
        validateActive();
        try {
            ArrayList<SftpFile> fileList = ls();
            int length = GenericUtil.count(fileList);
            SftpFile file, result = null;
            for (int i$ = 0; i$ < length; i$++) {
                file = fileList.get(i$);
                if (file.getFileName().equals(fileName)) {
                    result = file;
                }
            }
            LOGGER.debug("[{}] Check file [{}] exists: {}", hashCode, fileName, result);
            return result;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 显示sftp的当前路径. <br>
     * @return sftp当前路径 <br>
     */
    public String pwd() {
        validateActive();
        try {
            String pwd = sftpChannel.pwd();
            LOGGER.debug("[{}] Sftp pwd: {}", hashCode, pwd);
            return pwd;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 显示本地的当前路径. <br>
     * @return 本地当前路径 <br>
     */
    public String lpwd() {
        validateActive();
        try {
            String lpwd = sftpChannel.lpwd();
            LOGGER.debug("[{}] Sftp lpwd: {}", hashCode, lpwd);
            return lpwd;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 重命名. <br>
     * @param srcName 原文件名 <br>
     * @param destName 新文件名 <br>
     */
    public void rename(String srcName, String destName) {
        validateActive();
        try {
            LOGGER.debug("[{}] Rename [{}] to [{}]", hashCode, srcName, destName);
            sftpChannel.rename(srcName, destName);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 删除文件/文件夹. <br>
     * @param fileName 文件名 <br>
     */
    public void remove(String fileName) {
        validateActive();
        try {
            SftpFile file = ls(fileName);
            if (null == file) {
                LOGGER.debug("[{}] Cancel removing file because of not exists: {}", hashCode, fileName);
                return;
            }
            if (file.getFileAttributes().isDir()) {
                LOGGER.debug("[{}] Remove directory: {}", hashCode, fileName);
                try {
                    sftpChannel.rmdir(fileName);
                }
                catch (Throwable e) {
                    String command = "rm -r " + sftpChannel.pwd() + "/" + fileName;
                    execute(command, "utf-8");
                }
            }
            else {
                LOGGER.debug("[{}] Remove file: {}", hashCode, fileName);
                sftpChannel.rm(fileName);
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 执行命令行. <br>
     * @param command 命令行 <br>
     * @return <br>
     */
    public String execute(String command, String charSet) {
        validateActive();
        try {
            LOGGER.debug("[{}] Open exec channel", hashCode);
            execChannel = (ChannelExec) sshSession.openChannel("exec");
            LOGGER.debug("[{}] Execute command: [{}]", hashCode, command);
            execChannel.setCommand(command);
            execChannel.connect();
            String result = StringUtil.readFromInputStream(execChannel.getInputStream(), charSet);
            LOGGER.debug("[{}] Execution result: [{}]", hashCode, result);
            execChannel.disconnect();
            return result;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 以指定文件名上传本地文件到SFTP上. <br>
     * @param file 本地文件 <br>
     * @param destFileName 指定文件名 <br>
     */
    public void put(File file, String destFileName) {
        validateActive();
        LOGGER.debug("[{}] Put file from [{}] to [{}]", hashCode, file.getAbsolutePath(), destFileName);
        try (FileInputStream reader = new FileInputStream(file)) {
            sftpChannel.put(reader, destFileName);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 以指定文件名上传本地文件到SFTP上. <br>
     * @param filePath 本地文件路径 <br>
     * @param destFileName 指定文件名 <br>
     */
    public void put(String filePath, String destFileName) {
        put(new File(filePath), destFileName);
    }

    /**
     * Description: 以同名文件名上传本地文件到SFTP上. <br>
     * @param file 本地文件 <br>
     */
    public void put(File file) {
        put(file, file.getName());
    }

    /**
     * Description: 以同名文件名上传本地文件到SFTP上. <br>
     * @param filePath 本地文件路径 <br>
     */
    public void put(String filePath) {
        File file = new File(filePath);
        put(file);
    }

    /**
     * Description: 以指定文件名下载SFTP文件到本地. <br>
     * @param srcFileName SFTP文件 <br>
     * @param destFile 本地文件 <br>
     */
    public void get(String srcFileName, File destFile) {
        validateActive();
        LOGGER.debug("[{}] Get file from [{}] to [{}]", hashCode, srcFileName, destFile.getAbsolutePath());
        try (FileOutputStream writer = new FileOutputStream(destFile)) {
            sftpChannel.get(srcFileName, writer);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 以指定文件名下载SFTP文件到本地. <br>
     * @param srcFileName SFTP文件 <br>
     * @param destFileName 指定文件名 <br>
     */
    public void get(String srcFileName, String destFileName) {
        get(srcFileName, new File(destFileName));
    }

    /**
     * Description: 以同文件名下载SFTP文件到本地. <br>
     * @param srcFileName SFTP文件 <br>
     */
    public void get(String srcFileName) {
        get(srcFileName, new File(srcFileName));
    }

    @Override public void close() throws IOException {
        try {
            bye();
        }
        catch (Throwable e) {
            throw new IOException(e);
        }
    }
}
